package net.aiion.weave.impl.naming;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import net.aiion.weave.spi.bytecode.Bytecode;
import net.aiion.weave.spi.naming.Context;
import net.aiion.weave.spi.naming.Contextual;
import net.aiion.weave.spi.naming.ContextualException;
import net.aiion.weave.spi.naming.UserContext;
import net.aiion.weave.spi.naming.annotations.DynamicScoped;
import net.aiion.weave.spi.naming.annotations.NotScoped;
import net.aiion.weave.util.cryptography.SHA1;

public class ContextImpl implements Context
{
    private static final String NAMING_CONTEXT_NAME = Context.class.getName();

    private static final Class<NotScoped> NOT_SCOPED = NotScoped.class;
    private static final Class<DynamicScoped> DYNAMIC_SCOPED = DynamicScoped.class;

    private Class<? extends Annotation> scopeAnnotation;
    private String contextId;

    private Context parentContext;
    private Map<String, Context> childContexts;

    private Map<String, Class<? extends Contextual>> contextualNames;
    private Map<String, Contextual> boundContextuals;

    protected ContextImpl(final Class<? extends Annotation> aScopeAnnotation) {
        scopeAnnotation = aScopeAnnotation;
        contextId = SHA1.calculateString(String.valueOf(System.currentTimeMillis()) + UUID.randomUUID().toString());
        childContexts = new ConcurrentHashMap<>();
        boundContextuals = new HashMap<>();
    }

    @Override
    public Class<? extends Annotation> getScope() {
        return scopeAnnotation;
    }

    @Override
    public String getId() {
        return contextId;
    }

    @Override
    public UserContext createUserContext() {
        return new UserContextImpl(this);
    }

    @Override
    public final void setParentContext(final Context aContext) {
        parentContext = aContext;
        if (parentContext != null) {
            parentContext.addChildContext(this);
        }
    }

    @Override
    public final Context getParentContext() {
        return parentContext;
    }

    @Override
    public final void addChildContext(final Context aContext) {
        if (!childContexts.containsValue(aContext)) {
            childContexts.put(aContext.getId(), aContext);
            aContext.setParentContext(this);
        }
    }

    @Override
    public final void detachChildContext(final Context aContext) {
        if (childContexts != null && childContexts.remove(aContext.getId()) != null) {
            aContext.setParentContext(null);
        }
    }

    @Override
    public Context getChildContextById(final String aContextId) {
        if (aContextId == null) {
            return null;
        }

        return childContexts.get(aContextId);
    }

    @Override
    public final Collection<Context> getChildContexts() {
        if (childContexts == null) {
            return Collections.emptyList();
        }

        return Collections.unmodifiableCollection(childContexts.values());
    }

    @Override
    public final boolean containsBinding(final Class<?> aBindingType) {
        return containsBinding(aBindingType, aBindingType.getName());
    }

    @Override
    public final boolean containsBinding(final Class<?> aBindingType, final String aBindingName) {
        return boundContextuals.containsKey(aBindingName)
            && aBindingType.isInstance(boundContextuals.get(aBindingName));
    }

    @Override
    public final boolean containsBinding(final String aBindingName) {
        return boundContextuals.containsKey(aBindingName);
    }

    @Override
    public final Contextual lookupBinding(final Class<?> aBindingType) {
        return typeSafeBindingOrNull(aBindingType, lookupBinding(aBindingType.getName()));
    }

    @Override
    public final Contextual lookupBinding(final Class<?> aBindingType, final String aBindingName) {
        return typeSafeBindingOrNull(aBindingType, lookupBinding(aBindingName));
    }

    private Contextual typeSafeBindingOrNull(final Class<?> aBindingType, final Contextual aBindingInstance) {
        if (aBindingInstance != null && aBindingType.isInstance(aBindingInstance.getValue())) {
            return aBindingInstance;
        }

        return null;
    }

    @Override
    public final Contextual lookupBinding(final String aBindingName) {
        Contextual binding = lookupMap(aBindingName);
        if (binding == null) {
            binding = lookupContext(aBindingName);
        }

        if (binding == null) {
            binding = lookupContextualFactory(aBindingName);
        }

        if (binding == null) {
            binding = lookupParentContext(aBindingName);
        }

        return binding;
    }

    @Override
    public <T> Collection<T> lookupAll(final Collection<Class<? extends T>> aBindingTypes) {

        final Collection<T> foundBindings = new ArrayList<>(aBindingTypes.size());
        for (Class<? extends T> c : aBindingTypes) {
            final T o = lookup(c);
            if (o != null) {
                foundBindings.add(o);
            }
        }
        return foundBindings;
    }

    @Override
    public final <T> T lookup(final Class<T> aBindingType) {
        return typeSafeValueOrNull(aBindingType, lookupBinding(aBindingType.getName()));
    }

    @Override
    public final <T> T lookup(final Class<T> aBindingType, final String aBindingName) {
        return typeSafeValueOrNull(aBindingType, lookupBinding(aBindingName));
    }

    @SuppressWarnings("unchecked")
    private <T> T typeSafeValueOrNull(final Class<T> aBindingType, final Contextual aBindingInstance) {
        if (aBindingInstance != null && aBindingType.isInstance(aBindingInstance.getValue())) {
            return (T) aBindingInstance.getValue();
        }

        return null;
    }

    @Override
    public final Object lookup(final String aBindingName) {
        final Contextual binding = lookupBinding(aBindingName);
        if (binding == null) {
            return null;
        }

        return binding.getValue();
    }

    protected Contextual lookupContext(final String aBindingName) {
        Contextual binding = null;
        if (NAMING_CONTEXT_NAME.equals(aBindingName)) {
            binding = bind(aBindingName, this);
        }

        return binding;
    }

    private Contextual lookupContextualFactory(final String aContextualName) {
        return bind(createContextual(this, aContextualName));
    }

    private Contextual createContextual(final Context aContext, final String aContextualName) {
        if (!getContextualNames().containsKey(aContextualName)) {
            throw new ContextualException(String.format("Unknown contextual class for name '%s' requested.",
                aContextualName));
        }

        return createContextual(getContextualNames().get(aContextualName));
    }

    private Map<String, Class<? extends Contextual>> getContextualNames() {
        if (contextualNames != null) {
            return contextualNames;
        }

        contextualNames = new HashMap<>();
        for (Class<? extends Contextual> contextualClass : Bytecode.findClassesByInterfaceClass(Contextual.class)) {
            for (String contextualName : createContextual(contextualClass).getNames()) {
                if (contextualNames.containsKey(contextualName)) {
                    throw new ContextualException(String.format(
                        "Name '%s' is used for both contextual classes '%s' and '%s', but must be unique.",
                        contextualName, contextualNames.get(contextualName.getClass().getName()),
                        contextualClass.getName()));
                }

                contextualNames.put(contextualName, contextualClass);
            }
        }

        return contextualNames;
    }

    private Contextual createContextual(final Class<? extends Contextual> aContextualClass) {
        try {
            return aContextualClass.newInstance();
        } catch (final InstantiationException | IllegalAccessException e) {
            throw new ContextualException(e);
        }
    }

    private Contextual lookupParentContext(final String aBindingName) {
        if (parentContext == null) {
            return null;
        }

        Contextual binding = parentContext.lookupBinding(aBindingName);
        if (binding != null) {
            binding = bind(aBindingName, binding.getValue());
        }
        return binding;
    }

    private Contextual lookupMap(final String aBindingName) {
        return boundContextuals.get(aBindingName);
    }

    @Override
    public final Collection<Contextual> listBindings() {
        return Collections.unmodifiableCollection(boundContextuals.values());
    }

    @Override
    public final Collection<Contextual> listBindings(final Class<?> aBindingType) {
        final Collection<Contextual> bindings = new ArrayList<>();
        for (Contextual binding : boundContextuals.values()) {
            if (aBindingType.isInstance(binding.getValue())) {
                bindings.add(binding);
            }
        }
        return Collections.unmodifiableCollection(bindings);
    }

    @Override
    public final Collection<Object> list() {
        final Collection<Contextual> bindings = listBindings();
        final Collection<Object> bindingValues = new ArrayList<>(bindings.size());
        for (Contextual binding : bindings) {
            bindingValues.add(binding.getValue());
        }
        return Collections.unmodifiableCollection(bindingValues);
    }

    @Override
    public final Collection<Object> list(final Class<?> aBindingType) {
        final Collection<Contextual> bindings = listBindings(aBindingType);
        final Collection<Object> bindingValues = new ArrayList<>(bindings.size());
        for (Contextual binding : bindings) {
            bindingValues.add(binding.getValue());
        }
        return Collections.unmodifiableCollection(bindingValues);
    }

    @Override
    public final Contextual bind(final String aBindingName, final Object aBindingValue) {
        return bind(new ContextualValue(aBindingName, aBindingValue));
    }

    @Override
    public final Contextual bind(final Contextual aContextual) {
        final Class<? extends Annotation> contextualScope = aContextual.getScope();
        if (aContextual == null
            || (!NOT_SCOPED.equals(contextualScope) && !DYNAMIC_SCOPED.equals(contextualScope) && !scopeAnnotation
                .equals(contextualScope)))
        {
            return null;
        }

        if (NotScoped.class.equals(aContextual.getScope()) || boundContextuals.containsValue(aContextual)) {
            return aContextual;
        }

        for (String contextualName : aContextual.getNames()) {
            boundContextuals.put(contextualName, aContextual);
        }

        aContextual.onBound(this);
        return aContextual;
    }

    @Override
    public final Contextual unbind(final String aContextualName) {
        if (!boundContextuals.containsKey(aContextualName)) {
            return null;
        }

        final Contextual contextual = boundContextuals.get(aContextualName);
        for (String contextualName : contextual.getNames()) {
            boundContextuals.remove(contextualName);
        }
        contextual.onUnbound(this);

        return contextual;
    }

    @Override
    public void dispose() {
        disposeBindings();
        disposeChildContexts();
        detachParentContext();
    }

    private void disposeBindings() {
        for (String contextualName : new ArrayList<String>(boundContextuals.keySet())) {
            unbind(contextualName);
        }

        boundContextuals.clear();
    }

    private void disposeChildContexts() {
        if (childContexts == null) {
            return;
        }

        for (Context childContext : childContexts.values()) {
            disposeChildContext(childContext);
        }
        childContexts.clear();
    }

    private void disposeChildContext(final Context aChildContext) {
        aChildContext.setParentContext(null);
        aChildContext.dispose();
    }

    private void detachParentContext() {
        if (parentContext != null) {
            parentContext.detachChildContext(this);
            parentContext = null;
        }
    }
}
